﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;
using System.Web.Http.Description;
using System.Web.Http.OData;
using System.Web.Http.OData.Routing;
using System.Web.Http.Routing;
using Disco.Ontology;
using Microsoft.Data.OData;
using Microsoft.Data.OData.Query;

namespace Disco.Web.Api.Controllers.Dynamic
{
    [ApiExplorerSettings(IgnoreApi = false)]
    [BasicAuthentication]
    public abstract class DynamicControllerBase<TModel> : EntitySetController<TModel, long>
        where TModel : class, IEntity
    {

        #region Fields

        private OntologyContext _context = new OntologyContext();

        #endregion


        #region Properties

        protected virtual IQueryable<TModel> Model
        {
            get { return ModelSet; }
        }

        protected virtual IDbSet<TModel> ModelSet
        {
            get { return _context.Set<TModel>(); }
        }

        #endregion


        #region Methods

        private void SetCurrentOriginator()
        {
            // HINT: Getting the origin and user info should be done somewhere on authentification!
            var currentOrigin = GetCurrentOrigin();
            var currentUser = GetCurrentUser();

            var currentOriginator = _context.Originators.SingleOrDefault(originator =>
                originator.Origin.Key.Equals(currentOrigin.Key) &&
                originator.Author.Key.Equals(currentUser.Key));

            if (null == currentOriginator)
            {
                currentOriginator = new Ontology.Originator { Author = currentUser, Origin = currentOrigin };
                _context.Originators.Add(currentOriginator);
            }

            _context.SetCurrentOriginator(currentOriginator);
            _context.SaveChanges();
        }

        private Ontology.Origin GetCurrentOrigin()
        {
            if ((null == Request.Headers) || (null == Request.Headers.Referrer))
            {
                var message = "No referrer specified! Please set the HTTP request field 'referer' to an appropriate value to specify the origin of the request.";
                throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.Forbidden, message));
            }

            var originUri = Request.Headers.Referrer.AbsoluteUri;
            var currentOrigin = _context.Origins.SingleOrDefault(origin => originUri.StartsWith(origin.Uri));

            if (null == currentOrigin)
            {
                var message = string.Format("Origin not found! Please register an individual origin for your application. URI [{0}]", originUri);
                throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.Forbidden, message));
            }

            return currentOrigin;
        }

        private Ontology.User GetCurrentUser()
        {
            var currentUserIdentity = (UserIdentity)User.Identity;
            User currentUser;
            if (currentUserIdentity.User != null)
                currentUser = _context.Users.SingleOrDefault(user => user.Key == currentUserIdentity.User.Key);
            else
                currentUser = null;
            if (null == currentUser)
            {
                var message = string.Format("User not found! Please register an individual user for your application. Alias [{0}]", currentUserIdentity.RequestedName);
                throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.Unauthorized, message));
            }

            return currentUser;
        }

        public override IQueryable<TModel> Get()
        {
            return Model;
        }

        protected override long GetKey(TModel entity)
        {
            return entity.Id;
        }

        protected override TModel GetEntityByKey(long key)
        {
            TModel entity = Model.FirstOrDefault(item => item.Id.Equals(key));
            if (entity == null)
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }

            return entity;
        }

        protected virtual ICollection<TEntity> GetLinkCollection<TEntity>(long key, Func<TModel, ICollection<TEntity>> selector)
        {
            TModel entity = GetEntityByKey(key);
            return selector(entity);
        }

        protected virtual TEntity GetLink<TEntity>(long key, Func<TModel, TEntity> selector)
        {
            TModel entity = GetEntityByKey(key);

            TEntity result = selector(entity);
            if (result == null)
            {
                throw new HttpResponseException(HttpStatusCode.NoContent);
            }

            return result;
        }

        protected override TModel CreateEntity(TModel entity)
        {
            ModelSet.Add(entity);

            SetCurrentOriginator();
            _context.SaveChanges();

            return entity;
        }

        public override void CreateLink([FromODataUri] long key, string navigationProperty, [FromBody] Uri link)
        {
            var property = typeof(TModel).GetProperty(navigationProperty);
            if (null == property)
            {
                base.CreateLink(key, navigationProperty, link);
            }

            bool isManyNavigation = typeof(IEnumerable).IsAssignableFrom(property.PropertyType);

            Type partnerType = isManyNavigation ? property.PropertyType.GetGenericArguments()[0] : property.PropertyType;

            long partnerKey = GetKeyFromLinkUri(link);
            object partnerEntity = _context.Set(partnerType).Find(partnerKey);
            if (null == partnerEntity)
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }

            TModel entity = GetEntityByKey(key);

            if (isManyNavigation)
            {
                var methodInfo = property.PropertyType.GetMethod("Add", new Type[] { partnerType });
                methodInfo.Invoke(property.GetValue(entity), new object[] { partnerEntity });
            }
            else
            {
                property.SetValue(entity, partnerEntity);
            }

            SetCurrentOriginator();
            _context.SaveChanges();
        }

        private long GetKeyFromLinkUri(Uri link)
        {
            long key = -1;

            // Get the route that was used for this request.
            IHttpRoute route = Request.GetRouteData().Route;

            // Create an equivalent self-hosted route. 
            IHttpRoute newRoute = new HttpRoute(route.RouteTemplate,
                new HttpRouteValueDictionary(route.Defaults),
                new HttpRouteValueDictionary(route.Constraints),
                new HttpRouteValueDictionary(route.DataTokens), route.Handler);

            // Create a fake GET request for the link URI.
            var tmpRequest = new HttpRequestMessage(HttpMethod.Get, link);

            // Send this request through the routing process.
            var routeData = newRoute.GetRouteData(
                Request.GetConfiguration().VirtualPathRoot, tmpRequest);

            // If the GET request matches the route, use the path segments to find the key.
            if (routeData != null)
            {
                ODataPath path = tmpRequest.GetODataPath();
                var segment = path.Segments.OfType<KeyValuePathSegment>().FirstOrDefault();
                if (segment != null)
                {
                    // Convert the segment into the key type.
                    var keyAsObject = ODataUriUtils.ConvertFromUriLiteral(segment.Value, ODataVersion.V3);
                    key = Convert.ToInt64(keyAsObject);
                }
            }

            return key;
        }

        protected override TModel UpdateEntity(long key, TModel update)
        {
            // Verify that a product with this ID exists.
            if (!Model.Any(entity => entity.Id.Equals(key)))
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }

            ModelSet.Attach(update); // Replace the existing entity in the DbSet.

            _context.Entry(update).State = EntityState.Modified;

            SetCurrentOriginator();
            _context.SaveChanges();

            return update;
        }

        protected override TModel PatchEntity(long key, Delta<TModel> patch)
        {
            TModel entity = GetEntityByKey(key);
            patch.Patch(entity);

            SetCurrentOriginator();
            _context.SaveChanges();

            return entity;
        }

        public override void Delete([FromODataUri] long key)
        {
            TModel entity = GetEntityByKey(key);

            SetCurrentOriginator();
            _context.RemoveWithCascadeDelete(entity);
            _context.SaveChanges();
        }

        public override void DeleteLink([FromODataUri] long key, string navigationProperty, [FromBody] Uri link)
        {
            var property = typeof(TModel).GetProperty(navigationProperty);
            if (null == property)
            {
                base.DeleteLink(key, navigationProperty, link);
            }

            bool isManyNavigation = typeof(IEnumerable).IsAssignableFrom(property.PropertyType);
            if (isManyNavigation)
            {
                base.DeleteLink(key, navigationProperty, link);
            }

            TModel entity = GetEntityByKey(key);

            property.GetValue(entity); // HINT: It seems we need to get the value first to be able to set it to NULL!
            property.SetValue(entity, null);

            SetCurrentOriginator();
            _context.SaveChanges();
        }

        public override void DeleteLink(long key, string relatedKey, string navigationProperty)
        {
            var property = typeof(TModel).GetProperty(navigationProperty);
            if (null == property)
            {
                base.DeleteLink(key, relatedKey, navigationProperty);
            }

            bool isManyNavigation = typeof(IEnumerable).IsAssignableFrom(property.PropertyType);
            if (!isManyNavigation)
            {
                base.DeleteLink(key, relatedKey, navigationProperty);
            }

            Type partnerType = property.PropertyType.GetGenericArguments()[0];

            long partnerKey = Convert.ToInt64(relatedKey);
            object partnerEntity = _context.Set(partnerType).Find(partnerKey);
            if (null == partnerEntity)
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }

            TModel entity = GetEntityByKey(key);

            var methodInfo = property.PropertyType.GetMethod("Remove", new Type[] { partnerType });
            methodInfo.Invoke(property.GetValue(entity), new object[] { partnerEntity });

            SetCurrentOriginator();
            _context.SaveChanges();
        }

        #endregion

    }
}